﻿using System;
using System.ComponentModel;
using System.Net;
using System.Threading;


namespace CannedHeater.Model
{
    /// <summary>
    /// Handles messages to and from netdunio device
    /// Communication is done via headers in an HTTP Request, since WP7.0 does not support raw sockets
    /// </summary>
    public class DeviceRemoteControl : INotifyPropertyChanged
    {
        #region Locally used variables
        /// <summary>
        /// The uri at which the netduino device can be contacted
        /// </summary>
        private string _commandUri;

        /// <summary>
        /// Timer for autofetching data from netduino device
        /// </summary>
        private Timer _autoFetchTimer = null;

        /// <summary>
        /// The interval (in ms) at which data should be autofetched when connected
        /// </summary>
        private const int AUTOFETCH_INTERVAL = 30000;

        /// <summary>
        /// The number of retries before declaring a timeout
        /// </summary>
        private const int AUTOFETCH_TIMEOUT_TRY = 3;
        #endregion

        #region Types
        /// <summary>
        /// Holds a <see cref="WebClient"/> and a <see cref="DeviceRemoteControlCommand"/>.
        /// Used for transfering the state from the method starting an async web request,
        /// to the web response handler.
        /// </summary>
        struct RemoteControlCommandRequest
        {
            public WebClient webClient;
            public DeviceRemoteControlCommand command;
        }
        #endregion

        #region Events
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion

        #region Properties
        private int _autoFetchFailedAttempts = 0;
        /// <summary>
        /// The number of failed attempts to autofetch
        /// Automatically updates connected status when the <see cref="AUTOFETCH_TIMEOUT_TRY"/> is reached
        /// </summary>
        private int AutoFetchFailedAttempts
        {
            get { return _autoFetchFailedAttempts; }
            set
            {
                _autoFetchFailedAttempts++;
                if (_autoFetchFailedAttempts == AUTOFETCH_TIMEOUT_TRY)
                {
                    Connected = false;
                    StatusMsg = "Disconnected (timeout)";
                    _autoFetchFailedAttempts = 0;
                }
            }
        }

        private bool _connected = false;
        /// <summary>
        /// Whether or not a connection to the netduino is currently active
        /// Automatically starts/stops autofetching.
        /// </summary>
        public bool Connected
        {
            get { return _connected; }
            private set
            {
                _connected = value;
                if (value) StartAutoFetch();
                else StopAutoFetch();
                RaisePropertyChanged("Connected");
            }
        }

        private string _statusMsg = "Not connected...";
        /// <summary>
        /// A custom status message, representing status of the netduino (if connected)
        /// </summary>
        public string StatusMsg
        {
            get { return _statusMsg; }
            private set
            {
                _statusMsg = value;
                RaisePropertyChanged("StatusMsg");
            }
        }

        private DeviceSensorData _sensorData = new DeviceSensorData();
        /// <summary>
        /// The latest sensor data received from the netduino device
        /// </summary>
        public DeviceSensorData SensorData
        {
            get { return _sensorData; }
            set
            {
                _sensorData = value;
                RaisePropertyChanged("SensorData");
            }
        }
        #endregion

        #region Constructors
        /// <summary>
        /// Creates new DeviceRemoteControl for retrieving data and sending commands to netduino
        /// </summary>
        /// <param name="ip">The ip of the netduino device</param>
        /// <param name="port">The port on which the netdunio device is listening</param>
        public DeviceRemoteControl(string ip, int port)
        {
            _commandUri = "http://" + ip + ":" + port.ToString() + "/";
        }

        /// <summary>
        /// Creates new DeviceRemoteControl for retrieving data and sending commands to netduino
        /// </summary>
        /// <param name="ip">The ip of the netduino device</param>
        /// <param name="port">The port on which the netdunio device is listening</param>
        /// <param name="connected">Whether or not the netduino is connected</param>
        /// <param name="statusMsg">Custom status message</param>
        /// <param name="sensorData">The latest sensor data received from pachube</param>
        private DeviceRemoteControl(string ip, int port, bool connected, string statusMsg, DeviceSensorData sensorData) : this(ip, port)
        {
            StatusMsg = statusMsg;
            Connected = connected;
            SensorData = sensorData;
        }
        #endregion

        /// <summary>
        /// Constructs a new <see cref="DeviceRemoteControl"/>, reloading the state of a previous instance
        /// </summary>
        /// <param name="ip">The ip of the netduino device</param>
        /// <param name="port">The port on which the netdunio device is listening</param>
        /// <param name="connected">Whether or not the netduino is connected</param>
        /// <param name="statusMsg">Custom status message</param>
        /// <param name="sensorData">The latest sensor data received from pachube</param>
        /// <returns>A <see cref="DeviceRemoteControl"/> with old data reloaded</returns>
        public static DeviceRemoteControl RestoreController(string ip, int port, bool connected, string statusMsg, DeviceSensorData sensorData)
        {
            return new DeviceRemoteControl(ip, port, connected, statusMsg, sensorData);
        }

        /// <summary>
        /// Connect to the netduino device
        /// </summary>
        /// <remarks>Failed connection is reported via <see cref="StatusMsg"/></remarks>
        public void Connect()
        {
            // Actually, there is no open connection
            // Just verifies that the netduino is there, responding to requests.
            // Netduino does not push new data, instead this program is requesting data from the netduino via new requests at specific intervals (AUTOFETCH_INTERVAL)

            WebClient c = new WebClient();

            c.Headers["CH_ControlType"] = "ping"; // Connects simply by making sure the netduino is there to answer a 'ping'
            c.OpenReadCompleted += new OpenReadCompletedEventHandler(c_ConnectCompleted);

            StatusMsg = "Connecting...";

            c.OpenReadAsync(GetRandomizedUri(), c);

        }

        /// <summary>
        /// Handles the response from the attempt to open a new connection.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void c_ConnectCompleted(object sender, OpenReadCompletedEventArgs e)
        {
            WebClient c = (WebClient)e.UserState;
            if (e.Error == null)
            {
                if (c.ResponseHeaders["CH_Result"] == "pong") // Expected pong
                {
                    StatusMsg = "Connected";
                    Connected = true;
                }
                else
                {
                    StatusMsg = "Unexpected reply from device";
                }
            }
            else
            {
                StatusMsg = "Device not responding";
            }
        }

        /// <summary>
        /// Starts the auto fetching of latest data and status from netduino device
        /// First fetch immediatly after starting
        /// </summary>
        private void StartAutoFetch()
        {
            if (_autoFetchTimer != null) return; // Already started
            _autoFetchTimer = new Timer(new TimerCallback(DoAutoFetch), null, 0, AUTOFETCH_INTERVAL);
        }

        /// <summary>
        /// Stops automatic fetching the data and status from netduino device
        /// </summary>
        private void StopAutoFetch()
        {
            if (_autoFetchTimer == null) return; // Already stopped
            _autoFetchTimer.Dispose(); // Clear the timer
            _autoFetchTimer = null; // Indicate that the timer has been deactivated
        }

        /// <summary>
        /// Handles the autofetching, invoked by the <see cref="_autoFetchTimer"/> Timer
        /// </summary>
        /// <param name="o">State from timer startup - unused (null)</param>
        private void DoAutoFetch(object o)
        {
            FetchLatestDataAndStatus();
        }

        /// <summary>
        /// Sends a request to the netduino device, requesting latest data and status - starts async call, handled by <see cref="c_FetchLatestDataAndStatusCompleted"/>
        /// </summary>
        private void FetchLatestDataAndStatus()
        {
            WebClient c = new WebClient();

            c.Headers["CH_ControlType"] = "fetchLatestDataAndStatus"; // The message for the netduino device (overall command)
            c.OpenReadCompleted += new OpenReadCompletedEventHandler(c_FetchLatestDataAndStatusCompleted);

            // Use randomized URL (add ?rand=....) to avoid caching.
            // Sadly setting the Cache-Control header to no-cache doesn't seem to work
            // Sends the webclient along to the handler (e.UserState) for getting response headers later
            c.OpenReadAsync(GetRandomizedUri(), c); 
        }

        /// <summary>
        /// Handles the reply from the netduino device request.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void c_FetchLatestDataAndStatusCompleted(object sender, OpenReadCompletedEventArgs e)
        {
            WebClient c = (WebClient)e.UserState; // Easy to get responseheaders through the WebClient created with the request
            if (e.Error == null)
            {
                if (c.ResponseHeaders["CH_Result"] == "OK") // Expects OK
                {
                    // Do not expect any data in advance
                    double indoorTemp = Double.NaN;
                    double outdoorTemp = Double.NaN;
                    double solarHeaterTemp = Double.NaN;
                    double airFlow = Double.NaN;

                    // Try to get the data requested
                    double.TryParse(c.ResponseHeaders["CH_IndoorTemp"].Replace(',','.'), out indoorTemp);
                    double.TryParse(c.ResponseHeaders["CH_OutdoorTemp"].Replace(',', '.'), out outdoorTemp);
                    double.TryParse(c.ResponseHeaders["CH_SolarHeaterTemp"].Replace(',', '.'), out solarHeaterTemp);
                    double.TryParse(c.ResponseHeaders["CH_AirFlow"].Replace(',', '.'), out airFlow);

                    // Update the data stored locally
                    SensorData.IndoorTemp = indoorTemp;
                    SensorData.OutdoorTemp = outdoorTemp;
                    SensorData.SolarHeaterTemp = solarHeaterTemp;
                    SensorData.AirFlow = airFlow;

                    /*
                     * Handles current status of netduino device
                     */ 
                    string mode = c.ResponseHeaders["CH_Mode"];
                    // Prepare variables
                    int speed;
                    int time;
                    double temp;
                    try
                    {
                        switch (mode)
                        {
                            case "Auto":
                                // Get current speed and set status
                                speed = int.Parse(c.ResponseHeaders["CH_Speed"]);
                                SetStatus(new DeviceRemoteControlCommand() 
                                            {CommandType = DeviceRemoteControlCommandType.Auto, Speed = speed});
                                break;
                            case "Speed":
                                // Get the manually set speed, and set status
                                speed = int.Parse(c.ResponseHeaders["CH_Speed"]);
                                SetStatus(new DeviceRemoteControlCommand()
                                            { CommandType = DeviceRemoteControlCommandType.Speed, Speed = speed });
                                break;
                            case "SpeedTime":
                                // ...
                                speed = int.Parse(c.ResponseHeaders["CH_Speed"]);
                                time = int.Parse(c.ResponseHeaders["CH_Min"]);
                                SetStatus(new DeviceRemoteControlCommand()
                                            { CommandType = DeviceRemoteControlCommandType.SpeedTime, Speed = speed, Time = time });
                                break;

                            case "SpeedTemp":
                                speed = int.Parse(c.ResponseHeaders["CH_Speed"]);
                                temp = double.Parse(c.ResponseHeaders["CH_ToTemp"].Replace(',', '.'));
                                SetStatus(new DeviceRemoteControlCommand()
                                            { CommandType = DeviceRemoteControlCommandType.SpeedTemp, Speed = speed, Temp = temp });
                                break;
                        }
                    }catch
                    {
                        StatusMsg = "Warning: Malformed reply from device"; // Parsing of some data (int, double) failed, unexpected
                    }
                }
                else
                {
                    // Didn't get "OK"
                    AutoFetchFailedAttempts++;
                }
            }
            else
            {
                // Could not retrieve data
                AutoFetchFailedAttempts++;
            }
        }

        /// <summary>
        /// Execute a command on the netduino device (send request to device)
        /// </summary>
        /// <param name="cmd">The command to send</param>
        public void ExecCommand(DeviceRemoteControlCommand cmd)
        {
            WebClient c = new WebClient();

            c.Headers["CH_ControlType"] = "controlCommand"; // The overall indicator that a command will now be sent

            c.OpenReadCompleted += new OpenReadCompletedEventHandler(c_CommandExecResult);

            // Handle different command types
            // See description of the command types in the enum
            switch (cmd.CommandType)
            {
                case DeviceRemoteControlCommandType.Auto:
                    c.Headers["CH_ControlAction"] = "Auto";
                    break;

                case DeviceRemoteControlCommandType.Speed:
                    c.Headers["CH_ControlAction"] = "Speed";
                    c.Headers["CH_Speed"] = cmd.Speed.ToString();
                    break;

                case DeviceRemoteControlCommandType.SpeedTemp:
                    c.Headers["CH_ControlAction"] = "SpeedTemp";
                    c.Headers["CH_Speed"] = cmd.Speed.ToString();
                    c.Headers["CH_ToTemp"] = cmd.Temp.ToString().Replace(',', '.');
                    break;

                case DeviceRemoteControlCommandType.SpeedTime:
                    c.Headers["CH_ControlAction"] = "SpeedTime";
                    c.Headers["CH_Speed"] = cmd.Speed.ToString();
                    c.Headers["CH_Min"] = cmd.Temp.ToString();
                    break;
            }

            // Send request async, using randomized uri to avoid caching (no-cache not sufficient)
            // Uses tiny struct to forward the webclient and command to the handler (c_CommandExecResult) of the response (use e.UserState)
            c.OpenReadAsync(GetRandomizedUri(), new RemoteControlCommandRequest() { webClient = c, command = cmd });
        }

        /// <summary>
        /// Handles the reply from netduino device on a requested command execution
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void c_CommandExecResult(object sender, OpenReadCompletedEventArgs e)
        {
            // Get the original request
            RemoteControlCommandRequest req = (RemoteControlCommandRequest)e.UserState;
            WebClient c = req.webClient;

            if (e.Error == null)
            {
                if (c.ResponseHeaders["CH_Result"] == "OK") // Expects OK (command performed)
                {
                    SetStatus(req.command);
                }
                else
                {
                    StatusMsg = "Unexpected reply from device";
                }
            }
            else
            {
                StatusMsg = "Device not responding";
            }
        }

        /// <summary>
        /// Sets the custom status message according to the command
        /// </summary>
        /// <param name="cmd">The command for which a status message should be shown</param>
        private void SetStatus(DeviceRemoteControlCommand cmd)
        {
            // Depending on the command type, sset status message with the expected available data
            switch (cmd.CommandType)
            {
                case DeviceRemoteControlCommandType.Auto:
                    if (cmd.Speed != int.MinValue)
                        StatusMsg = "Auto mode, current speed: " + cmd.Speed.ToString();
                    else
                        StatusMsg = "Auto mode";
                    break;

                case DeviceRemoteControlCommandType.Speed:
                    StatusMsg = "Constant speed override: " + cmd.Speed.ToString();
                    break;

                case DeviceRemoteControlCommandType.SpeedTemp:
                    StatusMsg = "Speed " + cmd.Speed.ToString() + " until temp. is " +
                                cmd.Temp.ToString();
                    break;

                case DeviceRemoteControlCommandType.SpeedTime:
                    StatusMsg = "Speed " + cmd.Speed.ToString() + ", " + cmd.Time.ToString() +
                                " min. remaining";
                    break;
            }
        }

        #region Helpers
        /// <summary>
        /// Adds to the <see cref="_commandUri"/> a random parameter (rand=uniqueString), to prevent caching when sending requests
        /// No-cache not enough :(, therefore added GetRandomizedUri
        /// </summary>
        /// <returns></returns>
        private Uri GetRandomizedUri()
        {
            return new Uri(_commandUri + "?rand=" + DateTime.Now.Ticks.ToString());
        }

        public void RaisePropertyChanged(string prop)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(prop));
        }
        #endregion
    }

    /// <summary>
    /// The types of command that can be sent to the netduino device
    /// </summary>
    public enum DeviceRemoteControlCommandType
    {
        /// <summary>
        /// Automatic mode
        /// </summary>
        Auto,
        /// <summary>
        /// Manual override - constant speed
        /// </summary>
        Speed,
        /// <summary>
        /// Manual override - constant speed for a specified amount of time
        /// </summary>
        SpeedTime,
        /// <summary>
        /// Manual override - constant speed until indoor temperature reaches a specified temperature
        /// </summary>
        SpeedTemp
    }

    /// <summary>
    /// Represents a command to be sent to the netduino device.
    /// Class rather than struct - default values set
    /// </summary>
    public class DeviceRemoteControlCommand
    {
        // Suggested improvement: Make each command type be a new class, extending a general command class (possibly abstract)
        // Though very simple at this point, so maybe subclasses is overkill for now.

        /// <summary>
        /// The command type
        /// </summary>
        public DeviceRemoteControlCommandType CommandType;
        /// <summary>
        /// The manual override speed (in steps)
        /// </summary>
        public int Speed = int.MinValue;
        /// <summary>
        /// The manual override time (constant speed for <see cref="Time"/> min)
        /// </summary>
        public int Time; // min
        /// <summary>
        /// The manual override temperature (constnat speed until indoor temperature reached <see cref="Temp"/>)
        /// </summary>
        public double Temp;
    }

    /// <summary>
    /// Holds sensor data from the netduino device
    /// </summary>
    public class DeviceSensorData : INotifyPropertyChanged
    {
        // NaN is the default, able to determine if no data has been received yet.
        private double _indoorTemp = Double.NaN;
        private double _outdoorTemp = Double.NaN;
        private double _solarHeaterTemp = Double.NaN;
        private double _airFlow = Double.NaN;

        public double IndoorTemp
        {
            get { return _indoorTemp; }
            set
            {
                _indoorTemp = value;
                RaisePropertyChanged("IndoorTemp");
            }
        }

        public double OutdoorTemp
        {
            get { return _outdoorTemp; }
            set
            {
                _outdoorTemp = value;
                RaisePropertyChanged("OutdoorTemp");
            }
        }

        public double SolarHeaterTemp
        {
            get { return _solarHeaterTemp; }
            set
            {
                _solarHeaterTemp = value;
                RaisePropertyChanged("SolarHeaterTemp");
            }
        }

        public double AirFlow
        {
            get { return _airFlow; }
            set
            {
                _airFlow = value;
                RaisePropertyChanged("AirFlow");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public void RaisePropertyChanged(string prop)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(prop));
        }
    }
}
